fix refund bugs - #5
Conversation
|
レビューしました。結論として race condition の修正そのものは妥当 です。
その上で 3 点コメントします。1 と 2 は実装の指摘、3 はドキュメントです。 1. 有効期限ガードが bucket をロックしていない(
|
|
ご指摘ありがとうございます。3点とも対応しました。 1. 復元プラン作成時の SELECT がバケット行をロックしていない件
2. void も同じ復元処理を通るため、失効済みバケットがあると技術的な取消が恒久的に不可能になる件 復元先バケットの失効時ポリシーを分岐させました(
レスポンスは { "transaction_id": "…", "payment_id": "…", "amount": 500, "balance": 200,
"restorations": [ { "bucket_id": "…", "amount": 200 } ],
"expired": [ { "bucket_id": "…", "amount": 300 } ], "replayed": false }3. ドキュメントの記述が挙動と不一致な件
検証
なお Screencast.From.2026-08-12.14-15-01.webm |
|
更新分( 1.
|
|
指摘いただいた 3 点はすべて修正しました。 1.
|
|
|
該当箇所を 3 分岐に修正しました:
ご提案から変えたのは「支払いが使い切っていれば」→「使い切られていれば」の 1 語のみです。バケットは当該支払い以外の支払いでも残額 0 になりうるため、原因を特定しない表現にし、 同じ段落・同じ節で、上記と同種の不正確な記述が他に見つかったため、あわせて修正しました:
3 分岐はいずれも既存テストで固定されています:
挙動(コード)の変更はありません。ドキュメントとコメントの修正のみです。 なお、上記と同じ前提(失効済みバケットへの返金が「成立するが使えない残高になる」)が |
|
更新分( 今回追加・修正された記述はコードと突き合わせて確認し、いずれも正確でした。 1.
|
|
ご指摘の3点、および同種の記述が他に残っていた箇所をあわせて修正しました。 1.
|
|
更新分( 1. 返金額の入力で桁区切りが誤解釈されます(要修正・今回の変更で混入)
const input = prompt(
`返金額(円)。空欄で返金可能額 ${yen(refundable)} を返金します。`,
);
if (input === null) return;
const amount = input.trim() === "" ? null : parseInt(input, 10);表示された額をそのまま打ち返すと 入力側で 2.
|
|
ご指摘の3点を修正しました。 1. 返金額プロンプトの桁区切りが誤解釈される件表示だけ ご提案のうち表示を区切りなしに戻す方を採りました( 同じ画面の取消確認ダイアログは入力を伴わない確認なので 2.
|
概要
melon-dbの返金処理restore()(refund/void共通実装)に、独立した金銭正当性バグを 2 件見つけたので修正しました。restore()の隣接コードを触るため 1 本の PR にまとめています。バグ ① 並行返金による過剰返金
決済行をロックせず「返金済み額」を集計していたため、同一決済への並行返金が上限チェックの競合状態を通過し、過剰返金が成立していました。バケット共有時(例: ¥1000 バケットから ¥500 決済 2 件)に発生し、バケット側の
CHECK (remaining_amount <= original_amount)(migrations/0001_init.sql:59)は合計がバケット原資を超えない限り発火しないため捕まりません。pay()が候補バケットに既に行っている防御(ops.rs:1258のFOR UPDATE)を、restore()の決済行にも適用しました。再現テスト
concurrent_refunds_never_over_refundは修正前 10/10 回失敗(典型例: 5 件中 4 件が成功してしまう)。バグ ② 失効済みバケットへの返金
失効・スイープ済みバケットへ返金すると
statusが'active'に戻る一方expires_atは過去のまま残ります。balance()はexpires_at > nowでフィルタする(ops.rs:343)ため、利用者からは残高が消えたまま見えず、次回スイープで同じ金額が再度失効益に計上されます(二重計上)。返金計画の段階で対象バケットの
expires_atを確認し、過去なら新エラーDbError::RefundIntoExpiredBucket(422REFUND_INTO_EXPIRED_BUCKET)で返金全体を拒否するようにしました。部分成立は許しません。再現テスト
refund_does_not_resurrect_an_expired_bucketで、修正前は返金が成功し直後の再スイープで同額(¥400)が再計上されることを確認しています。変更
fix refund race condition— バグ①reorder refund restoration steps— 挙動不変のリファクタ(バグ②の下ごしらえ、返金計画を書き込み前に確定)reject refund into an expired bucket— バグ②、およびmelon-server/docs/api.md/端末 UI への反映各コミットは単体で clippy・全テストが通ります(
git rebase --execで確認)。検証
cargo fmt --all --check/cargo clippy --workspace --all-targets --locked -- -D warnings/cargo test --workspace --locked全通過既知の制約
フォークからの PR には
FELICA_RS_TOKENリポジトリシークレットが渡らないため、この PR の CI (.github/workflows/ci.yml) は失敗します(ワークフロー冒頭のコメントに明記されている既知の制約)。ローカルでは上記の通り全チェック通過済みです。